// Copyright 2008-2009 Louis DeJardin - http://whereslou.com
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// 
using System;
using System.Collections.Generic;
using System.Linq;
using Spark.Compiler;
using Spark.Compiler.ChunkVisitors;
using Spark.Parser.Code;
using Spark;

#pragma warning disable 10005 

namespace NRails.Spark.ChunkVisitors
{
    public module ExtConditionalType
    {
        public AlternativeIf : int = 99;
        public AlternativeElseIf : int = 98;
    }

    public class GeneratedCodeVisitor : GeneratedCodeVisitorBase
    {
        private mutable _source : SourceWriter;
        private mutable _nullBehaviour : NullBehaviour;

        public this(source : SourceWriter, globalSymbols : Dictionary[string, object], nullBehaviour : NullBehaviour)
        {
            _nullBehaviour = nullBehaviour;
            _source = source;
            def globalScope = Scope(null);
            globalScope.Variables = globalSymbols;
            _scope = Scope(globalScope);
        }

        private CodeIndent(chunk : Chunk) : SourceWriter
        {
            when (_source.AdjustDebugSymbols)
            {
                when (chunk != null && chunk.Position != null)
                {
                    _source.StartOfLine = false;
                    _source.WriteDirective("#line {0} \"{1}\"", chunk.Position.Line, chunk.Position.SourceContext.FileName)
                        .Indent(chunk.Position.Column - 1);
                }
                _source.StartOfLine = false;
                _ = _source.WriteLine("#line default");
            }
            _source;
        }

        private CodeHidden() : void
        {/*
            if (_source.AdjustDebugSymbols)
                _source.WriteDirective("#line hidden");*/
        }

        private CodeDefault() : void
        {
            when (_source.AdjustDebugSymbols)
                _ = _source.WriteDirective("#line default");
        }

        private AppendOpenBrace() : void
        {
            PushScope();
            _ = _source.WriteLine("{").AddIndent();
        }

        private AppendCloseBrace() : void
        {
            _ = _source.RemoveIndent().WriteLine("}");
            PopScope();
        }

        class Scope
        {
            public this(prior : Scope)
            {
                Variables = Dictionary.[string, object]();
                Prior = prior;
            }
            public Variables : Dictionary[string, object] { get; set; }
            public Prior : Scope { get; private set; }
        }

        private mutable _scope : Scope;

        PushScope() : void
        {
            _scope = Scope(_scope);
        }
        PopScope() : void
        {
            _scope = _scope.Prior;
        }
        DeclareVariable(name : string) : void
        {
            _scope.Variables.Add(name, null);
        }
        IsVariableDeclared(name : string) : bool
        {
            mutable scan = _scope;
            while (scan != null)
            {
                when (scan.Variables.ContainsKey(name))
                     true;
                
                scan = scan.Prior;
            }
            false;
        }


        protected override Visit(chunk : SendLiteralChunk) : void
        {
            if (string.IsNullOrEmpty(chunk.Text))
            {}
            else
            {
                CodeHidden();
                _ = _source.Write("Output.Write(\"").Write(EscapeStringContents(chunk.Text)).WriteLine("\");");
                CodeDefault();
            }
        }

        protected override Visit(chunk : SendExpressionChunk) : void
        {
            mutable automaticallyEncode = chunk.AutomaticallyEncode;
            when (chunk.Code.ToString().StartsWith("H("))
                automaticallyEncode = false;

            _ = _source
                .WriteLine("try")
                .WriteLine("{");
            _ = CodeIndent(chunk)
                .Write("Output.Write(")
                .Write(if (automaticallyEncode) "H(" else "")
                .WriteCode(chunk.Code)
                .Write(if (automaticallyEncode) ")" else "")
                .WriteLine(");");
            CodeDefault();
            _ = _source
                .WriteLine("}");

            if (_nullBehaviour == NullBehaviour.Lenient)
            {
                _source.WriteLine("catch");
                AppendOpenBrace();
                _source.WriteLine(" | _ is System.NullReferenceException => ");
                if (!chunk.SilentNulls)
                {
                    _source.Write("Output.Write(\"${")
                        .Write(EscapeStringContents(chunk.Code))
                        .WriteLine("}\");");
                }
                else
                {
                    _source.WriteLine("{}");
                }
                AppendCloseBrace();
            }
            else
            {
                _source.WriteLine("catch");
                AppendOpenBrace();
                _source.Write("| ex is System.NullReferenceException => throw System.ArgumentNullException(\"${")
                    .Write(EscapeStringContents(chunk.Code))
                    .WriteLine("}\", ex);");
                AppendCloseBrace();
            }
        }

        static EscapeStringContents(text : string) : string
        {
            text.Replace("\\", "\\\\").Replace("\t", "\\t").Replace("\r", "\\r").Replace("\n", "\\n").Replace("\"", "\\\"");
        }

        protected override Visit(_ : MacroChunk) : void
        {

        }

        protected override Visit(chunk : CodeStatementChunk) : void
        {
            CodeIndent(chunk).WriteCode(chunk.Code).WriteLine();
            CodeDefault();
        }

        protected override Visit(chunk : LocalVariableChunk) : void
        {
            when (chunk.Type.ToString() == "var")
                chunk.Type = "def";

            DeclareVariable(chunk.Name);

            CodeIndent(chunk).WriteCode(TypeHelper.CorrectType(chunk.Type)).Write(" ").WriteCode(chunk.Name);
            when (!Snippets.IsNullOrEmpty(chunk.Value))
            {
                _source.Write(" = ").WriteCode(chunk.Value);
            }
            _source.WriteLine(";");
            CodeDefault();
        }

        protected override Visit(_ : UseMasterChunk) : void
        {
            //no-op
        }

        protected override Visit(chunk : DefaultVariableChunk) : void
        {
            when (chunk.Type.ToString() == "var")
                chunk.Type = "def";

            if (IsVariableDeclared(chunk.Name))
            {}
            else 
            {
                DeclareVariable(chunk.Name);
                CodeIndent(chunk).WriteCode(TypeHelper.CorrectType(chunk.Type)).Write(" ").Write(chunk.Name);
                when (!Snippets.IsNullOrEmpty(chunk.Value))
                {
                    _source.Write(" = ").WriteCode(chunk.Value);
                }
                _source.WriteLine(";");
                CodeDefault();
            }
        }

        protected override Visit(chunk : ForEachChunk) : void
        {
            def terms = chunk.Code.ToString().Split(' ', '\r', '\n', '\t').ToList();
            def inIndex = terms.IndexOf("in");
            
            def variableName = if (inIndex < 2)
                null;
            else
                terms[inIndex - 1];
                
            if (variableName == null)
            {
                CodeIndent(chunk)
                    .Write("foreach(")
                    .WriteCode(chunk.Code)
                    .WriteLine(")");
                CodeDefault();
                AppendOpenBrace();
                Accept(chunk.Body);
                AppendCloseBrace();
            }
            else
            {
                def detect = DetectCodeExpressionVisitor(OuterPartial);
                def autoIndex = detect.Add(variableName + "Index");
                def autoCount = detect.Add(variableName + "Count");
                def autoIsFirst = detect.Add(variableName + "IsFirst");
                def autoIsLast = detect.Add(variableName + "IsLast");
                detect.Accept(chunk.Body);

                when (autoIsLast.Detected)
                {
                    autoIndex.Detected = true;
                    autoCount.Detected = true;
                }

                AppendOpenBrace();
                when (autoIndex.Detected)
                {
                    DeclareVariable(variableName + "Index");
                    _source.WriteLine("def {0}Index = 0;", variableName);
                }
                when (autoIsFirst.Detected)
                {
                    DeclareVariable(variableName + "IsFirst");
                    _source.WriteLine("def {0}IsFirst = true;", variableName);
                }
                when (autoCount.Detected)
                {
                    DeclareVariable(variableName + "Count");
                    def collectionCode = string.Join(" ", terms.ToArray(), inIndex + 1, terms.Count - inIndex - 1);
                    _source.WriteLine("def {0}Count = Spark.Compiler.CollectionUtility.Count({1});", variableName, collectionCode);
                }


                CodeIndent(chunk)
                    .Write("foreach(")
                    .WriteCode(chunk.Code)
                    .WriteLine(")");
                CodeDefault();

                AppendOpenBrace();
                DeclareVariable(variableName);

                CodeHidden();
                when (autoIsLast.Detected)
                {
                    DeclareVariable(variableName + "IsLast");
                    _source.WriteLine("def {0}IsLast = ({0}Index == {0}Count - 1);", variableName);
                }
                CodeDefault();

                Accept(chunk.Body);

                CodeHidden();
                when (autoIndex.Detected)
                    _source.WriteLine("++{0}Index;", variableName);
                when (autoIsFirst.Detected)
                    _source.WriteLine("{0}IsFirst = false;", variableName);
                CodeDefault();

                AppendCloseBrace();

                AppendCloseBrace();
            }
        }

        protected override Visit(chunk : ScopeChunk) : void
        {
            AppendOpenBrace();
            CodeDefault();
            Accept(chunk.Body);
            AppendCloseBrace();
        }

        protected override Visit(_ : GlobalVariableChunk) : void
        {
        }

        protected override Visit(chunk : AssignVariableChunk) : void
        {
            CodeIndent(chunk)
                .Write(chunk.Name)
                .Write(" = ")
                .WriteCode(chunk.Value)
                .WriteLine(";");
            CodeDefault();
        }


        protected override Visit(chunk : ContentChunk) : void
        {
            CodeIndent(chunk).WriteLine("using(OutputScope(\"{0}\"))", chunk.Name);
            CodeDefault();
            AppendOpenBrace();
            Accept(chunk.Body);
            AppendCloseBrace();
        }

        protected override Visit(_ : UseImportChunk) : void
        {

        }

        protected override Visit(chunk : ContentSetChunk) : void
        {
            CodeIndent(chunk).WriteLine("using(OutputScope(System.IO.StringWriter()))");
            CodeDefault();

            AppendOpenBrace();

            Accept(chunk.Body);

            CodeHidden();
            def format = match (chunk.AddType)
            {
                | ContentAddType.AppendAfter =>
                    "{0} = {0} + Output.ToString();";
                | ContentAddType.InsertBefore =>
                    "{0} = Output.ToString() + {0};";
                | _ =>
                    "{0} = Output.ToString();";
            }
            _source.WriteLine(format, chunk.Variable);

            AppendCloseBrace();

            CodeDefault();
        }

        protected override Visit(chunk : UseContentChunk) : void
        {
            CodeIndent(chunk).WriteLine(string.Format("when (Content.ContainsKey(\"{0}\"))", chunk.Name));
            CodeHidden();
            AppendOpenBrace();
            _source.WriteFormat("Spark.Spool.TextWriterExtensions.WriteTo(Content[\"{0}\"], Output);", chunk.Name);
            AppendCloseBrace();

            when (chunk.Default.Count != 0)
            {
                _source.WriteLine("else");
                AppendOpenBrace();
                Accept(chunk.Default);
                AppendCloseBrace();
            }
            CodeDefault();
        }


        protected override  Visit(_ : ViewDataChunk) : void
        {

        }

        protected override  Visit(_ : UseNamespaceChunk) : void
        {

        }

        protected override Visit(_ : UseAssemblyChunk): void
        {

        }

        protected override Visit(chunk : ExtensionChunk) : void
        {
            chunk.Extension.VisitChunk(this, OutputLocation.RenderMethod, chunk.Body, _source.GetStringBuilder());
        }

        protected override Visit(chunk : ConditionalChunk) : void
        {
            match (chunk.Type)
            {
                | ConditionalType.If =>
                        CodeIndent(chunk)
                            .Write("when (")
                            .WriteCode(chunk.Condition)
                            .WriteLine(")");
                | ConditionalType.ElseIf =>
                        CodeIndent(chunk)
                            .Write("else when (")
                            .WriteCode(chunk.Condition)
                            .WriteLine(")");
                | ConditionalType.Else =>
                        _source.WriteLine("else");
                | ConditionalType.Once =>
                        CodeIndent(chunk)
                            .Write("when (Once(")
                            .WriteCode(chunk.Condition)
                            .WriteLine("))");
                | x => 
                    match (x :> int)
                    {
                        | ExtConditionalType.AlternativeIf =>
                                CodeIndent(chunk)
                                    .Write("if (")
                                    .WriteCode(chunk.Condition)
                                    .WriteLine(")");
                        | ExtConditionalType.AlternativeElseIf =>
                                CodeIndent(chunk)
                                    .Write("else when (")
                                    .WriteCode(chunk.Condition)
                                    .WriteLine(")");
                        | _ =>
                            throw CompilerException($"Unexpected conditional type $(chunk.Type)");
                    }
            }
            CodeDefault();
            AppendOpenBrace();
            Accept(chunk.Body);
            AppendCloseBrace();
        }

        protected override Visit(chunk : CacheChunk) : void
        {
            def siteGuid = Guid.NewGuid();

            CodeIndent(chunk)
                .Write("when (BeginCachedContent(\"")
                .Write(siteGuid.ToString("n"))
                .Write("\", Spark.CacheExpires(")
                .WriteCode(chunk.Expires)
                .Write("), ")
                .WriteCode(chunk.Key)
                .WriteLine("))")
                .WriteLine("{").AddIndent();

            _source
                .WriteLine("try");
            
            AppendOpenBrace();
            Accept(chunk.Body);
            AppendCloseBrace();

            _source
                .WriteLine("finally")
                .WriteLine("{").AddIndent()
                .Write("EndCachedContent(")
                .WriteCode(chunk.Signal)
                .Write(");")
                .RemoveIndent().WriteLine("}")
                .RemoveIndent().WriteLine("}");
        }

        
    }

}
#pragma warning restore 10005 
